feat: Add deeplinks support and Raycast extension#1569
feat: Add deeplinks support and Raycast extension#1569njg7194 wants to merge 1 commit intoCapSoftware:mainfrom
Conversation
This PR adds comprehensive deeplink support for Cap and a Raycast extension to control Cap from Raycast. ## Deeplinks Added (deeplink_actions.rs) New deeplink actions: - `pause_recording` - Pause current recording - `resume_recording` - Resume paused recording - `toggle_pause` - Toggle pause state - `set_microphone` - Switch microphone input - `set_camera` - Switch camera input - `take_screenshot` - Take a screenshot of primary display - `show_main_window` - Show main Cap window - `list_displays` - List available displays - `list_windows` - List available windows - `list_microphones` - List available microphones - `list_cameras` - List available cameras - `get_recording_status` - Get current recording status ## Raycast Extension (extensions/raycast-cap/) Commands included: - Start Recording - Start a new screen recording - Stop Recording - Stop current recording - Pause Recording - Pause current recording - Resume Recording - Resume paused recording - Toggle Pause - Toggle pause state - Take Screenshot - Capture screen - Open Cap - Open the Cap application - Open Settings - Open Cap settings - Recording Controls - Quick access list view Deeplink format: `cap-desktop://action?value=<JSON_ENCODED_ACTION>` Closes CapSoftware#1540
| /// Response types for deeplink queries | ||
| #[derive(Debug, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub struct RecordingStatusResponse { | ||
| pub is_recording: bool, | ||
| pub is_paused: bool, | ||
| pub mode: Option<RecordingMode>, | ||
| } |
There was a problem hiding this comment.
[P1] RecordingStatusResponse/DisplayInfo/etc are never used and (more importantly) the deep link “query” actions (List*, GetRecordingStatus) don’t return anything to the caller. Right now they only trace! and Ok(()), which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 18:25
Comment:
[P1] `RecordingStatusResponse`/`DisplayInfo`/etc are never used and (more importantly) the deep link “query” actions (`List*`, `GetRecordingStatus`) don’t return anything to the caller. Right now they only `trace!` and `Ok(())`, which makes them effectively no-ops from the deeplink consumer’s perspective (Raycast can’t read stdout/stderr/tracing). This also means these new structs add dead code unless a return mechanism is introduced.
How can I resolve this? If you propose a fix, please make it concise.| DeepLinkAction::ListDisplays => { | ||
| let displays: Vec<DisplayInfo> = cap_recording::screen_capture::list_displays() | ||
| .into_iter() | ||
| .map(|(d, _)| DisplayInfo { | ||
| name: d.name.clone(), | ||
| id: format!("{:?}", d.id), | ||
| }) | ||
| .collect(); | ||
| // Log for debugging; in practice this could be returned via a different mechanism | ||
| trace!("Available displays: {:?}", displays); | ||
| Ok(()) |
There was a problem hiding this comment.
[P2] ListDisplays formats d.id with format!("{:?}", d.id) instead of a stable identifier. If DisplayId’s Debug impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 256:266
Comment:
[P2] `ListDisplays` formats `d.id` with `format!("{:?}", d.id)` instead of a stable identifier. If `DisplayId`’s `Debug` impl changes across versions/platforms, the value becomes non-portable for consumers (e.g., a Raycast command that wants to persist an ID). Consider exposing the underlying id type/string in a stable way.
How can I resolve this? If you propose a fix, please make it concise.| DeepLinkAction::TakeScreenshot => { | ||
| // Take a screenshot of the primary display | ||
| let displays = cap_recording::screen_capture::list_displays(); | ||
| if let Some((display, _)) = displays.into_iter().next() { | ||
| let target = ScreenCaptureTarget::Display { id: display.id }; | ||
| crate::recording::take_screenshot(app.clone(), target) | ||
| .await | ||
| .map(|_| ()) | ||
| } else { | ||
| Err("No display found for screenshot".to_string()) | ||
| } |
There was a problem hiding this comment.
[P2] TakeScreenshot picks list_displays().into_iter().next() as “primary display”. list_displays() ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 235:245
Comment:
[P2] `TakeScreenshot` picks `list_displays().into_iter().next()` as “primary display”. `list_displays()` ordering isn’t guaranteed, so this can capture a non-primary monitor depending on platform/ordering. If “primary” matters, it should be selected explicitly (or allow a display id/name parameter).
How can I resolve this? If you propose a fix, please make it concise.| export function buildDeeplinkUrl(action: DeepLinkAction): string { | ||
| const actionValue = | ||
| typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action); | ||
| const encodedValue = encodeURIComponent(actionValue); | ||
| return `${CAP_DEEPLINK_SCHEME}://action?value=${encodedValue}`; | ||
| } |
There was a problem hiding this comment.
[P3] buildDeeplinkUrl has a redundant conditional: typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action). It always does the same thing, so the typeof check can be removed to reduce noise.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/utils/deeplink.ts
Line: 56:61
Comment:
[P3] `buildDeeplinkUrl` has a redundant conditional: `typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action)`. It always does the same thing, so the `typeof` check can be removed to reduce noise.
How can I resolve this? If you propose a fix, please make it concise.| import { ActionPanel, Action, List, Icon, Color, showToast, Toast } from "@raycast/api"; | ||
| import { executeCapAction, isCapInstalled, openCap } from "./utils/deeplink"; | ||
| import { useEffect, useState } from "react"; |
There was a problem hiding this comment.
[P3] Unused imports: showToast and Toast are imported but never used. This will trip linters (ray lint) and adds noise.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast-cap/src/recording-controls.tsx
Line: 1:3
Comment:
[P3] Unused imports: `showToast` and `Toast` are imported but never used. This will trip linters (`ray lint`) and adds noise.
How can I resolve this? If you propose a fix, please make it concise.| const [isInstalled, setIsInstalled] = useState<boolean | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| isCapInstalled().then(setIsInstalled); |
There was a problem hiding this comment.
Consider handling isCapInstalled() errors + avoiding state updates after unmount; also makes showToast/Toast imports meaningful.
| isCapInstalled().then(setIsInstalled); | |
| useEffect(() => { | |
| let active = true; | |
| void (async () => { | |
| try { | |
| const installed = await isCapInstalled(); | |
| if (active) setIsInstalled(installed); | |
| } catch (error) { | |
| if (!active) return; | |
| setIsInstalled(false); | |
| await showToast({ | |
| style: Toast.Style.Failure, | |
| title: "Failed to detect Cap", | |
| message: error instanceof Error ? error.message : "Unknown error", | |
| }); | |
| } | |
| })(); | |
| return () => { | |
| active = false; | |
| }; | |
| }, []); |
| await executeCapAction( | ||
| { | ||
| start_recording: { | ||
| capture_mode: { screen: "Main Display" }, |
There was a problem hiding this comment.
Heads up: capture_mode.screen needs to exactly match Cap's display names (from cap_recording::screen_capture::list_displays()); hardcoding "Main Display" may not resolve on many systems. Might be worth supporting it as an alias in the deeplink handler or switching to ID-based selection.
| */ | ||
| export function buildDeeplinkUrl(action: DeepLinkAction): string { | ||
| const actionValue = | ||
| typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action); |
There was a problem hiding this comment.
Minor: this branch is redundant (both sides do JSON.stringify).
| typeof action === "string" ? JSON.stringify(action) : JSON.stringify(action); | |
| const actionValue = JSON.stringify(action); |
| } | ||
|
|
||
| try { | ||
| await open("", CAP_BUNDLE_ID); |
There was a problem hiding this comment.
Launching via a deeplink is usually more reliable than open("", bundleId) (it also gets you into a known UI state).
| await open("", CAP_BUNDLE_ID); | |
| await open(buildDeeplinkUrl("show_main_window")); |
| Window(String), | ||
| } | ||
|
|
||
| /// Response types for deeplink queries |
There was a problem hiding this comment.
Repo style seems to avoid code comments; these new ////// comments in the deeplink handler might be worth removing for consistency.
Deeplinks Support + Raycast Extension
Closes #1540
Summary
This PR adds comprehensive deeplink support for Cap and a complete Raycast extension to control Cap directly from Raycast.
Changes
1. Extended Deeplink Actions (
apps/desktop/src-tauri/src/deeplink_actions.rs)Added the following new deeplink actions:
pause_recordingresume_recordingtoggle_pauseset_microphoneset_cameratake_screenshotshow_main_windowlist_displayslist_windowslist_microphoneslist_camerasget_recording_status2. Raycast Extension (
extensions/raycast-cap/)A complete Raycast extension with the following commands:
Deeplink Format
Examples
Stop Recording:
Pause Recording:
Toggle Pause:
Start Recording:
Testing
Test Deeplinks (macOS Terminal)
Test Raycast Extension
cd extensions/raycast-cap npm install npm run devScreenshots
(Add screenshots of Raycast extension here)
Checklist
Greptile Overview
Greptile Summary
This PR adds new Cap deeplink actions on the desktop side (pause/resume/toggle, input switching, screenshots, window/display/device listing, and recording status) and introduces a new Raycast extension that triggers these actions by opening
cap-desktop://URLs.The Raycast extension is straightforward (each command calls a shared deeplink helper), and the desktop handler wires the new actions into existing recording/window APIs.
Main issue to address: the new “list/query” deeplink actions (
list_displays,list_windows,list_microphones,list_cameras,get_recording_status) only emittrace!logs and returnOk(()), so consumers like Raycast cannot actually receive the requested data. There are also smaller correctness/maintainability nits (e.g., choosing the first display as “primary”,Debug-formatted IDs, and a couple of unused/redundant TS bits).Confidence Score: 3/5
Important Files Changed
Sequence Diagram
sequenceDiagram participant Raycast as Raycast Extension participant OS as macOS URL Handler participant Cap as Cap Desktop (Tauri) participant DL as deeplink_actions::handle participant Rec as recording subsystem Raycast->>Raycast: buildDeeplinkUrl(action) Raycast->>OS: open(cap-desktop://action?value=...) OS->>Cap: deliver URL event Cap->>DL: handle(app_handle, urls) DL->>DL: DeepLinkAction::try_from(url) alt start_recording DL->>Rec: set_camera_input / set_mic_input DL->>Rec: start_recording(inputs) else stop/pause/resume/toggle DL->>Rec: stop_recording / pause_recording / resume_recording / toggle_pause_recording else take_screenshot DL->>Rec: list_displays() & take_screenshot(target) else list/query actions DL->>DL: list_* / get_recording_status DL-->>DL: trace!(...) (no return to Raycast) end DL-->>Cap: Result logged on error(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!
Context used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)